//
//   Copyright 2011 Gynvael Coldwind & Mateusz "j00ru" Jurczyk
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
//
#include <cstdio>
#include <cstdlib>
#include <string>
#include <set>
#include <assert.h>
#include "j00ru.h"
#include "gynvael.h"
#include "player.h"
#include "global.h"
#include "record.h"
#include "music.h"
using namespace std;


bool isTileForbidden(enum TILE_TYPE type, int x, int y)
{
  int id;

  if(x < 0 || y < 0) return true;
  if(x >= g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->width) return true;
  if(y >= g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->height) return true;

  switch(type)
  {
    case TILE_WALL:
    case TILE_TOGGLE_BUTTON:
    case TILE_NOTHING:
      return true;

    case TILE_HOLE:
      id = g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[y][x].params[2];
      if(id == 0)
        return true;
      return false;

    case TILE_DOOR_V:
    case TILE_DOOR_H:
      id = g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[y][x].params[0];

      if(g_ObjectParams[id][DOOR_OPEN])
        return false;
      else
        return true;

      break;

    default:
      return false;
  }

  return false;
}


void player_InitGameState()
{
  if(g_GameState.currentLevel == NULL)
    return;

  if(g_GameState.players != NULL)
    delete g_GameState.players;

  g_GameState.players      = new PLAYER_STRUCT[g_GameState.currentLevel->playerCount];
  g_GameState.activePlayer = 0;
  g_GameState.timeLeft     = g_GameState.currentLevel->deadline;
 
  for( int i = 0; i < g_GameState.currentLevel->playerCount; i++ )
  {
    g_GameState.players[i].map = 0;
    g_GameState.players[i].pos = 0;
  }

  player_InitializeAllXY();
}

void player_InitializeXY(int player, bool reloadParams)
{
  for( int y = 0; y < g_GameState.currentLevel->maps[g_GameState.players[player].map]->height; y++ )
  {
    for( int x = 0; x < g_GameState.currentLevel->maps[g_GameState.players[player].map]->width; x++ )
    {
      if(g_GameState.currentLevel->maps[g_GameState.players[player].map]->tiles[y][x].tileType == TILE_START_POINT &&
         g_GameState.currentLevel->maps[g_GameState.players[player].map]->tiles[y][x].params[0] == player )
      {
        g_GameState.players[player].x = ((float)x + 0.5f);
        g_GameState.players[player].y = ((float)y + 0.5f);

        if(reloadParams)
        {
          g_GameState.players[player].params[0] = g_GameState.currentLevel->maps[g_GameState.players[player].map]->tiles[y][x].params[1];
          g_GameState.players[player].params[1] = g_GameState.currentLevel->maps[g_GameState.players[player].map]->tiles[y][x].params[2];
          g_GameState.players[player].params[2] = g_GameState.currentLevel->maps[g_GameState.players[player].map]->tiles[y][x].params[3];
        }
      }
    }
  }
}

void player_InitializeAllXY()
{
  // For each player, search for a TILE_START_POINT tile with a corresponding id parameter
  for( int i = 0; i < g_GameState.currentLevel->playerCount; i++ )
  {
    player_InitializeXY(i, true);
  }
}

void player_UpdatePosition()
{
  float x = g_GameState.players[g_GameState.activePlayer].x;
  float y = g_GameState.players[g_GameState.activePlayer].y;
  float new_x, new_y;

  // this is used to determin how to animate the playes.
  unsigned int *move_marker = (unsigned int *)&g_GameState.players[g_GameState.activePlayer].pos;
  *move_marker = (*move_marker) & 0x7fffffff; // Remove the "has moved" bit.


  if(keys['w']        && keys['d']) *move_marker = 0x80000001;
  else if(keys['s'] && keys['d']) *move_marker = 0x80000003;
  else if(keys['s'] && keys['a'] ) *move_marker = 0x80000005;
  else if(keys['w']   && keys['a'] ) *move_marker = 0x80000007;
  else if(keys['w']                      ) *move_marker = 0x80000000;
  else if(                   keys['d']) *move_marker = 0x80000002;
  else if(keys['s']                    ) *move_marker = 0x80000004;
  else if(                   keys['a'] ) *move_marker = 0x80000006;
  

  // Detect which keys are pressed and how to move the current player
  if(keys['w'])
  {
    new_y = g_GameState.players[g_GameState.activePlayer].y - PLAYER_SPEED * ratio;


    // Check if the player is trying to exceed map bounds
    if(new_y >= HALF_PLAYER_SIZE)
    {
      // Check if both upper tiles are allowed
      int tile_x, tile_y;

      tile_x = (int)(x - HALF_PLAYER_SIZE);
      tile_y = (int)(new_y - HALF_PLAYER_SIZE);

      assert(tile_x >= 0);
      assert(tile_y >= 0);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto down;

      tile_x = (int)(g_GameState.players[g_GameState.activePlayer].x + HALF_PLAYER_SIZE);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto down;
    }
    
    g_GameState.players[g_GameState.activePlayer].y = new_y;
  }
  
down:
  if(keys['s'])
  {
    new_y = g_GameState.players[g_GameState.activePlayer].y + PLAYER_SPEED * ratio;

    if(new_y <= (float)g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->height - HALF_PLAYER_SIZE)
    {
      // Check if both lower tiles are allowed
      int tile_x, tile_y;

      tile_x = (int)(x - HALF_PLAYER_SIZE);
      tile_y = (int)(new_y + HALF_PLAYER_SIZE);

      assert(tile_x >= 0);
      assert(tile_y >= 0);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto left;

      tile_x = (int)(g_GameState.players[g_GameState.activePlayer].x + HALF_PLAYER_SIZE);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto left;

      g_GameState.players[g_GameState.activePlayer].y = new_y;
    }
  }

left:
  if(keys['a'])
  {
    new_x = g_GameState.players[g_GameState.activePlayer].x - PLAYER_SPEED * ratio;

    if(new_x >= HALF_PLAYER_SIZE)
    {
      // Check if both lower tiles are allowed
      int tile_x, tile_y;

      tile_x = (int)(new_x - HALF_PLAYER_SIZE);
      tile_y = (int)(y - HALF_PLAYER_SIZE);

      assert(tile_x >= 0);
      assert(tile_y >= 0);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto right;

      tile_y = (int)(g_GameState.players[g_GameState.activePlayer].y + HALF_PLAYER_SIZE);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto right;

      g_GameState.players[g_GameState.activePlayer].x = new_x;
    }
  }

right:
  if(keys['d'])
  {
    new_x = g_GameState.players[g_GameState.activePlayer].x + PLAYER_SPEED * ratio;

    if(new_x <= (float)g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->width - HALF_PLAYER_SIZE)
    {
      // Check if both lower tiles are allowed
      int tile_x, tile_y;

      tile_x = (int)(new_x + HALF_PLAYER_SIZE);
      tile_y = (int)(y - HALF_PLAYER_SIZE);

      assert(tile_x >= 0);
      assert(tile_y >= 0);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto ret;

      tile_y = (int)(g_GameState.players[g_GameState.activePlayer].y + HALF_PLAYER_SIZE);

      if(isTileForbidden(g_GameState.currentLevel->maps[g_GameState.currentLevel->activeMap]->tiles[tile_y][tile_x].tileType, tile_x, tile_y))
        goto ret;
    }

    g_GameState.players[g_GameState.activePlayer].x = new_x;
  }

ret:
  if(keys[SDLK_SPACE] &&
     g_GameState.activePlayer < g_GameState.currentLevel->playerCount - 1)
  {
    g_GameState.activePlayer = ((g_GameState.activePlayer + 1) % (g_GameState.currentLevel->playerCount));
    keys[SDLK_SPACE] = false;

    // Do some magic with the recorders
    rec_SaveCurrentTrack(&g_Recorder);
    rec_Reset(&g_Recorder);

    // Restore the initial state
    rec_RestoreInitState(&g_Recorder);

    // Renew the current player's deadline
    g_GameState.timeLeft = g_GameState.currentLevel->deadline;
  }

  return;
}

bool player_IsSpellAllowed(int playerType, int spell)
{
#define CHARACTER_TYPES 4
  int spellAbility[CHARACTER_TYPES];

  spellAbility[PLAYER_TYPE_ONE]   = OBJECT_TYPE_SPELL_FIREBALL;
  spellAbility[PLAYER_TYPE_TWO]   = OBJECT_TYPE_SPELL_BULLET;
  spellAbility[PLAYER_TYPE_THREE] = OBJECT_TYPE_SPELL_TELEPORT;
  spellAbility[PLAYER_TYPE_FOUR]  = OBJECT_TYPE_SPELL_BRIDGE;

  if(playerType >= CHARACTER_TYPES)
    return false;

  return (spellAbility[playerType] & spell);
}

void player_HandleSpells()
{
  int spell = -1;
  PLAYER_STRUCT *p = &g_GameState.players[g_GameState.activePlayer];

  int type = p->params[0];
  int count_left = p->params[1];
  if(!lmb) 
    return;

  if(count_left <= 0)
    return; // Nope.

  switch(type)
  {
    case 0: // Fireball
      spell = OBJECT_TYPE_SPELL_FIREBALL;
      lmb = false;
      count_left--;      
      break;

    case 1: // Bullet
      spell = OBJECT_TYPE_SPELL_BULLET;
      lmb = false;
      count_left--;
      break;

    case 2: // Teleport
      {
        lmb = false;

        int map_id = g_GameState.currentLevel->activeMap;
         MAP_STRUCT *map = g_GameState.currentLevel->maps[map_id];
        if(target_floor_x < 0 || target_floor_y < 0 ||
           target_floor_x >= map->width ||
           target_floor_y >= map->height ||
           map->tiles[target_floor_y][target_floor_x].tileType != TILE_FLOOR)
          return; // Nevermind

        TILE_STRUCT *t = &map->tiles[target_floor_y][target_floor_x];
        t->tileType = TILE_TELEPORT;
        t->params[0] = count_left == 2 ? 98 : 99;
        t->params[1] = count_left == 2 ? 99 : -1;
        g_ObjectParams[t->params[0]][0] = t->params[1];

        rec_InsertModifyTile(&g_Recorder, target_floor_x, target_floor_y, map_id, t);
        rec_InsertModifyParams(&g_Recorder, t->params[0], 0, t->params[1]);

        count_left--;        
      }
      break;

    case 3: // Bridge
      {
        lmb = false;

        int map_id = g_GameState.currentLevel->activeMap;
         MAP_STRUCT *map = g_GameState.currentLevel->maps[map_id];
        if(target_floor_x < 0 || target_floor_y < 0 ||
           target_floor_x >= map->width ||
           target_floor_y >= map->height ||
           map->tiles[target_floor_y][target_floor_x].tileType != TILE_HOLE ||
           map->tiles[target_floor_y][target_floor_x].params[2] == 1
           )
          return; // Nevermind

        // Is the player close enough?
        float dist_sq =
          (target_floor_x - p->x) * (target_floor_x - p->x) +
          (target_floor_y - p->y) * (target_floor_y - p->y);

        if(dist_sq > 3.0f * 3.0f)
        {
          music_playSound(SND_DENY);
          return; // Too far.
        }


        TILE_STRUCT *t = &map->tiles[target_floor_y][target_floor_x];
        t->params[2] = 1;

        rec_InsertModifyTile(&g_Recorder, target_floor_x, target_floor_y, map_id, t);
        
        count_left--;                
      }
      break;
  }

  p->params[1] = count_left;

  if(spell != -1)
  {
    if(player_IsSpellAllowed(p->params[0], spell))
    {
      OBJECT_STRUCT* obj;
      
      switch(spell)
      {
        case OBJECT_TYPE_SPELL_BULLET:
          obj = object_CreateBullet(p->x, p->y, g_GameState.currentLevel->activeMap, target_vx, target_vy);
          break;

        case OBJECT_TYPE_SPELL_FIREBALL:
          obj = object_CreateFireball(p->x, p->y, g_GameState.currentLevel->activeMap, target_vx, target_vy);
          break;

        case OBJECT_TYPE_SPELL_TELEPORT:
        case OBJECT_TYPE_SPELL_BRIDGE:
          break;
      }
      
      EVENT_STRUCT ev;
      ev.type = EVENT_CREATE_OBJECT;
      ev.tick = g_Ticks;
      memcpy(&ev.u.createObject, obj, sizeof(OBJECT_STRUCT));
      rec_Insert(&g_Recorder, &ev);

      music_playSound(SND_SHOOT);
      
      printf("[+] Player %u using spell=%u\n", g_GameState.activePlayer, spell);
    }
    else
    {
      printf("[-] Player %u not allowed to use spell=%u\n", g_GameState.activePlayer, spell);
    }
  }
}

void player_HandleSpecialLocations()
{
  for( int map = 0; map < g_GameState.currentLevel->mapCount; map++ )
  {

    // Go through all map tiles and handle special cases (such as std buttons or exits)
    for( int y = 0; y < g_GameState.currentLevel->maps[map]->height; y++ )
    {
      for( int x = 0; x < g_GameState.currentLevel->maps[map]->width; x++ )
      {
        vector<int> collidingPlayers;
        bool activeFound = false;

        // XXX: Only these two types of tiles need to be specially handled so far
        if(g_GameState.currentLevel->maps[map]->tiles[y][x].tileType != TILE_STD_BUTTON &&
           g_GameState.currentLevel->maps[map]->tiles[y][x].tileType != TILE_END_POINT &&
           g_GameState.currentLevel->maps[map]->tiles[y][x].tileType != TILE_EXIT &&
           g_GameState.currentLevel->maps[map]->tiles[y][x].tileType != TILE_TELEPORT)
          continue;

        // Tele only if you can enter it.
        if(g_GameState.currentLevel->maps[map]->tiles[y][x].tileType == TILE_TELEPORT)
        {
          int dst_tele = g_GameState.currentLevel->maps[map]->tiles[y][x].params[0];
          dst_tele = g_ObjectParams[dst_tele][0];
          if(dst_tele == -1)
            continue; // This is an out-teleport
        }

        for( int i = 0; i < g_GameState.currentLevel->playerCount; i++ )
        {
          set<pair<int, int> > tiles;
          int tile_x[2], tile_y[2];

          if(g_GameState.players[i].map != map)
            continue;

          // Check if any of the maximum four tiles occupied by player are special
          tile_x[0] = (int)(g_GameState.players[i].x - HALF_PLAYER_SIZE);
          tile_x[1] = (int)(g_GameState.players[i].x + HALF_PLAYER_SIZE);

          tile_y[0] = (int)(g_GameState.players[i].y - HALF_PLAYER_SIZE);
          tile_y[1] = (int)(g_GameState.players[i].y - HALF_PLAYER_SIZE);

          tiles.insert(make_pair(tile_x[0], tile_y[0]));
          tiles.insert(make_pair(tile_x[0], tile_y[1]));
          tiles.insert(make_pair(tile_x[1], tile_y[0]));
          tiles.insert(make_pair(tile_x[1], tile_y[1]));

          for( set<pair<int, int> >::iterator It = tiles.begin(); It != tiles.end(); It++ )
          {
            if(It->first == x &&
               It->second == y)
            {
              if(i == g_GameState.activePlayer)
                activeFound = true;

              collidingPlayers.push_back(i);
              break;
            }
          }
        }

        if(g_GameState.currentLevel->maps[map]->tiles[y][x].tileType == TILE_STD_BUTTON)
        {
          if(collidingPlayers.empty())
          {
            if(g_ObjectParams[g_GameState.currentLevel->maps[map]->tiles[y][x].params[0]][DOOR_OPEN] != false &&
               map == g_GameState.currentLevel->activeMap)
               music_playSound(SND_DOOR);

            g_ObjectParams[g_GameState.currentLevel->maps[map]->tiles[y][x].params[0]][DOOR_OPEN] = false;
          }
          else
          {
            if(g_ObjectParams[g_GameState.currentLevel->maps[map]->tiles[y][x].params[0]][DOOR_OPEN] != true &&
               map == g_GameState.currentLevel->activeMap)
              music_playSound(SND_DOOR);

            g_ObjectParams[g_GameState.currentLevel->maps[map]->tiles[y][x].params[0]][DOOR_OPEN] = true;
          }
        }
        else if(g_GameState.currentLevel->maps[map]->tiles[y][x].tileType == TILE_TELEPORT && collidingPlayers.size())
        {
          size_t cnt = collidingPlayers.size();          
          int dst_tele = g_GameState.currentLevel->maps[map]->tiles[y][x].params[0];
          dst_tele = g_ObjectParams[dst_tele][0];

          int dst_x, dst_y, dst_map;
          gynvael_GetTeleportDst(dst_tele, &dst_x, &dst_y, &dst_map);

          if(dst_x != -1)
          {

            for(size_t m = 0; m < cnt; m++)
            {
              int pidx = collidingPlayers[m];
              g_GameState.players[pidx].x = float(dst_x) + 0.5f;
              g_GameState.players[pidx].y = float(dst_y) + 0.5f;              
              g_GameState.players[pidx].map = dst_map;

              // Switch map for active player.
              if(pidx == g_GameState.activePlayer)
                g_GameState.currentLevel->activeMap = dst_map;
            }

            music_playSound(SND_TELEPORT);
          }
        }
        else if(g_GameState.currentLevel->maps[map]->tiles[y][x].tileType == TILE_END_POINT)
        {
          // We need to move the players to the next map (if it exists).
          // In case the current players is one of the colliding ones, activate the next map.

          if(collidingPlayers.empty())
            continue;
  
          // Change the players' current map and re-initialize their coords.
          for( unsigned i = 0; i < collidingPlayers.size(); i++ )
          {
            g_GameState.players[collidingPlayers[i]].map++;
            player_InitializeXY(collidingPlayers[i], false);

            if(collidingPlayers[i] == g_GameState.activePlayer)
            {
              if(g_GameState.currentLevel->activeMap == g_GameState.currentLevel->mapCount - 1)
              {
                // if we encounter an "end" on the last map of a level, treat it as an exit
                puts("debug: \"end\" tile encountered on the last map on level; treating as \"exit\"");
                goto exit_triggered;
              }
              g_GameState.currentLevel->activeMap++;
            }
          }
        }
        else if(g_GameState.currentLevel->maps[map]->tiles[y][x].tileType == TILE_EXIT)
        {
          if(collidingPlayers.empty())
            continue;

exit_triggered:
          // Clean the g_Object list
          for( int i = 0; i < g_Objects.size(); i++ )
            delete g_Objects[i];
          g_Objects.clear();

          // Free the current level descriptor
          for( int i = 0; i < g_GameState.currentLevel->mapCount; i++ )
            delete g_GameState.currentLevel->maps[i];
          delete g_GameState.currentLevel->maps;
          delete g_GameState.currentLevel;

          // Increment level id
          g_GameState.levelNumber++;

          // level completed
          game_SwitchMode(GAME_MODE_LVL_FINISHED);
          return;
        }
      }
    }
  }
}

void player_RenderAll()
{
  for(uint8_t i = 0; i < g_GameState.currentLevel->playerCount; i++)
    gynvael_RenderPlayer(i);  
}

